/*
 * Released under BSD License
 * Copyright (c) 2014-2016 hizzgdev@163.com
 *
 * Project Home:
 *   https://github.com/hizzgdev/jsmind/
 */

;
(function($w) {
	'use strict';
	// set 'jsMind' as the library name.
	// __name__ should be a const value, Never try to change it easily.
	var __name__ = 'jsMind';
	// library version
	var __version__ = '0.4.6';
	// author
	var __author__ = 'hizzgdev@163.com';

	// an noop function define
	var _noop = function() {};
	var logger = (typeof console === 'undefined') ? {
		log: _noop,
		debug: _noop,
		error: _noop,
		warn: _noop,
		info: _noop
	} : console;

	// check global variables
	if(typeof module === 'undefined' || !module.exports) {
		if(typeof $w[__name__] != 'undefined') {
			logger.log(__name__ + ' has been already exist.');
			return;
		}
	}

	// shortcut of methods in dom
	var $d = $w.document;
	var $g = function(id) {
		return $d.getElementById(id);
	};
	var $c = function(tag) {
		return $d.createElement(tag);
	};
	var $t = function(n, t) {
		if(n.hasChildNodes()) {
			n.firstChild.nodeValue = t;
		} else {
			n.appendChild($d.createTextNode(t));
		}
	};
	var $h = function(n, t) {
		n.innerHTML = t;
	};
	// detect isElement
	var $i = function(el) {
		return !!el && (typeof el === 'object') && (el.nodeType === 1) && (typeof el.style === 'object') && (typeof el.ownerDocument === 'object');
	};
	if(typeof String.prototype.startsWith != 'function') {
		String.prototype.startsWith = function(p) {
			return this.slice(0, p.length) === p;
		};
	}

	var DEFAULT_OPTIONS = {
		container: '', // id of the container
		editable: false, // you can change it in your options
		theme: null,
		mode: 'full', // full or side
		support_html: true,

		view: {
			hmargin: 100,
			vmargin: 50,
			line_width: 2,
			line_color: '#555'
		},
		layout: {
			hspace: 30,
			vspace: 20,
			pspace: 13
		},
		default_event_handle: {
			enable_mousedown_handle: true,
			enable_click_handle: true,
			enable_dblclick_handle: true
		},
		shortcut: {
			enable: true,
			handles: {},
			mapping: {
				addchild: 45, // Insert
				addbrother: 13, // Enter
				editnode: 113, // F2
				delnode: 46, // Delete
				toggle: 32, // Space
				left: 37, // Left
				up: 38, // Up
				right: 39, // Right
				down: 40, // Down
			}
		},
	};

	// core object
	var jm = function(options) {
		jm.current = this;

		this.version = __version__;
		var opts = {};
		jm.util.json.merge(opts, DEFAULT_OPTIONS);
		jm.util.json.merge(opts, options);

		if(!opts.container) {
			logger.error('the options.container should not be null or empty.');
			return;
		}
		this.options = opts;
		this.inited = false;
		this.mind = null;
		this.event_handles = [];
		this.init();
	};

	// ============= static object =============================================
	jm.direction = {
		left: -1,
		center: 0,
		right: 1
	};
	jm.event_type = {
		show: 1,
		resize: 2,
		edit: 3,
		select: 4
	};

	jm.node = function(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
		if(!sId) {
			logger.error('invalid nodeid');
			return;
		}
		if(typeof iIndex != 'number') {
			logger.error('invalid node index');
			return;
		}
		if(typeof bExpanded === 'undefined') {
			bExpanded = true;
		}
		this.id = sId;
		this.index = iIndex;
		this.topic = sTopic;
		this.data = oData || {};
		this.isroot = bIsRoot;
		this.parent = oParent;
		this.direction = eDirection;
		this.expanded = !!bExpanded;
		this.children = [];
		this._data = {};
	};

	jm.node.compare = function(node1, node2) {
		// '-1' is alwary the last
		var r = 0;
		var i1 = node1.index;
		var i2 = node2.index;
		if(i1 >= 0 && i2 >= 0) {
			r = i1 - i2;
		} else if(i1 == -1 && i2 == -1) {
			r = 0;
		} else if(i1 == -1) {
			r = 1;
		} else if(i2 == -1) {
			r = -1;
		} else {
			r = 0;
		}
		//logger.debug(i1+' <> '+i2+'  =  '+r);
		return r;
	};

	jm.node.inherited = function(pnode, node) {
		if(!!pnode && !!node) {
			if(pnode.id === node.id) {
				return true;
			}
			if(pnode.isroot) {
				return true;
			}
			var pid = pnode.id;
			var p = node;
			while(!p.isroot) {
				p = p.parent;
				if(p.id === pid) {
					return true;
				}
			}
		}
		return false;
	};

	jm.node.prototype = {
		get_location: function() {
			var vd = this._data.view;
			return {
				x: vd.abs_x,
				y: vd.abs_y
			};
		},
		get_size: function() {
			var vd = this._data.view;
			return {
				w: vd.width,
				h: vd.height
			}
		}
	};

	jm.mind = function() {
		this.name = null;
		this.author = null;
		this.version = null;
		this.root = null;
		this.selected = null;
		this.nodes = {};
	};

	jm.mind.prototype = {
		get_node: function(nodeid) {
			if(nodeid in this.nodes) {
				return this.nodes[nodeid];
			} else {
				logger.warn('the node[id=' + nodeid + '] can not be found');
				return null;
			}
		},

		set_root: function(nodeid, topic, data) {
			if(this.root == null) {
				this.root = new jm.node(nodeid, 0, topic, data, true);
				this._put_node(this.root);
			} else {
				logger.error('root node is already exist');
			}
		},

		add_node: function(parent_node, nodeid, topic, data, idx, direction, expanded) {
			if(!jm.util.is_node(parent_node)) {
				var the_parent_node = this.get_node(parent_node);
				if(!the_parent_node) {
					logger.error('the parent_node[id=' + parent_node + '] can not be found.');
					return null;
				} else {
					return this.add_node(the_parent_node, nodeid, topic, data, idx, direction, expanded);
				}
			}
			var nodeindex = idx || -1;
			var node = null;
			if(parent_node.isroot) {
				var d = jm.direction.right;
				if(isNaN(direction)) {
					var children = parent_node.children;
					var children_len = children.length;
					var r = 0;
					for(var i = 0; i < children_len; i++) {
						if(children[i].direction === jm.direction.left) {
							r--;
						} else {
							r++;
						}
					}
					d = (children_len > 1 && r > 0) ? jm.direction.left : jm.direction.right
				} else {
					d = (direction != jm.direction.left) ? jm.direction.right : jm.direction.left;
				}
				node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, d, expanded);
			} else {
				node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, parent_node.direction, expanded);
			}
			if(this._put_node(node)) {
				parent_node.children.push(node);
				this._reindex(parent_node);
			} else {
				logger.error('fail, the nodeid \'' + node.id + '\' has been already exist.');
				node = null;
			}
			return node;
		},

		insert_node_before: function(node_before, nodeid, topic, data) {
			if(!jm.util.is_node(node_before)) {
				var the_node_before = this.get_node(node_before);
				if(!the_node_before) {
					logger.error('the node_before[id=' + node_before + '] can not be found.');
					return null;
				} else {
					return this.insert_node_before(the_node_before, nodeid, topic, data);
				}
			}
			var node_index = node_before.index - 0.5;
			return this.add_node(node_before.parent, nodeid, topic, data, node_index);
		},

		get_node_before: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return null;
				} else {
					return this.get_node_before(the_node);
				}
			}
			if(node.isroot) {
				return null;
			}
			var idx = node.index - 2;
			if(idx >= 0) {
				return node.parent.children[idx];
			} else {
				return null;
			}
		},

		insert_node_after: function(node_after, nodeid, topic, data) {
			if(!jm.util.is_node(node_after)) {
				var the_node_after = this.get_node(node_before);
				if(!the_node_after) {
					logger.error('the node_after[id=' + node_after + '] can not be found.');
					return null;
				} else {
					return this.insert_node_after(the_node_after, nodeid, topic, data);
				}
			}
			var node_index = node_after.index + 0.5;
			return this.add_node(node_after.parent, nodeid, topic, data, node_index);
		},

		get_node_after: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return null;
				} else {
					return this.get_node_after(the_node);
				}
			}
			if(node.isroot) {
				return null;
			}
			var idx = node.index;
			var brothers = node.parent.children;
			if(brothers.length >= idx) {
				return node.parent.children[idx];
			} else {
				return null;
			}
		},

		move_node: function(node, beforeid, parentid, direction) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return null;
				} else {
					return this.move_node(the_node, beforeid, parentid, direction);
				}
			}
			if(!parentid) {
				parentid = node.parent.id;
			}
			return this._move_node(node, beforeid, parentid, direction);
		},

		_flow_node_direction: function(node, direction) {
			if(typeof direction === 'undefined') {
				direction = node.direction;
			} else {
				node.direction = direction;
			}
			var len = node.children.length;
			while(len--) {
				this._flow_node_direction(node.children[len], direction);
			}
		},

		_move_node_internal: function(node, beforeid) {
			if(!!node && !!beforeid) {
				if(beforeid == '_last_') {
					node.index = -1;
					this._reindex(node.parent);
				} else if(beforeid == '_first_') {
					node.index = 0;
					this._reindex(node.parent);
				} else {
					var node_before = (!!beforeid) ? this.get_node(beforeid) : null;
					if(node_before != null && node_before.parent != null && node_before.parent.id == node.parent.id) {
						node.index = node_before.index - 0.5;
						this._reindex(node.parent);
					}
				}
			}

			return node;
		},

		_move_node: function(node, beforeid, parentid, direction) {
			if(!!node && !!parentid) {
				if(node.parent.id != parentid) {
					// remove from parent's children
					var sibling = node.parent.children;
					var si = sibling.length;
					while(si--) {
						if(sibling[si].id == node.id) {
							sibling.splice(si, 1);
							break;
						}
					}
					node.parent = this.get_node(parentid);
					node.parent.children.push(node);
				}

				if(node.parent.isroot) {
					if(direction == jsMind.direction.left) {
						node.direction = direction;
					} else {
						node.direction = jm.direction.right;
					}
				} else {
					node.direction = node.parent.direction;
				}
				this._move_node_internal(node, beforeid);
				this._flow_node_direction(node);
			}

			return node;
		},

		remove_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return false;
				} else {
					return this.remove_node(the_node);
				}
			}
			if(!node) {
				logger.error('fail, the node can not be found');
				return false;
			}
			if(node.isroot) {
				logger.error('fail, can not remove root node');
				return false;
			}
			if(this.selected != null && this.selected.id == node.id) {
				this.selected = null;
			}
			// clean all subordinate nodes
			var children = node.children;
			var ci = children.length;
			while(ci--) {
				this.remove_node(children[ci]);
			}
			// clean all children
			children.length = 0;
			// remove from parent's children
			var sibling = node.parent.children;
			var si = sibling.length;
			while(si--) {
				if(sibling[si].id == node.id) {
					sibling.splice(si, 1);
					break;
				}
			}
			// remove from global nodes
			delete this.nodes[node.id];
			// clean all properties
			for(var k in node) {
				delete node[k];
			}
			// remove it's self
			node = null;
			//delete node;
			return true;
		},

		_put_node: function(node) {
			if(node.id in this.nodes) {
				logger.warn('the nodeid \'' + node.id + '\' has been already exist.');
				return false;
			} else {
				this.nodes[node.id] = node;
				return true;
			}
		},

		_reindex: function(node) {
			if(node instanceof jm.node) {
				node.children.sort(jm.node.compare);
				for(var i = 0; i < node.children.length; i++) {
					node.children[i].index = i + 1;
				}
			}
		},
	};

	jm.format = {
		node_tree: {
			example: {
				"meta": {
					"name": __name__,
					"author": __author__,
					"version": __version__
				},
				"format": "node_tree",
				"data": {
					"id": "root",
					"topic": "jsMind Example"
				}
			},
			get_mind: function(source) {
				var df = jm.format.node_tree;
				var mind = new jm.mind();
				mind.name = source.meta.name;
				mind.author = source.meta.author;
				mind.version = source.meta.version;
				df._parse(mind, source.data);
				return mind;
			},
			get_data: function(mind) {
				var df = jm.format.node_tree;
				var json = {};
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				};
				json.format = 'node_tree';
				json.data = df._buildnode(mind.root);
				return json;
			},

			_parse: function(mind, node_root) {
				var df = jm.format.node_tree;
				var data = df._extract_data(node_root);
				mind.set_root(node_root.id, node_root.topic, data);
				if('children' in node_root) {
					var children = node_root.children;
					for(var i = 0; i < children.length; i++) {
						df._extract_subnode(mind, mind.root, children[i]);
					}
				}
			},

			_extract_data: function(node_json) {
				var data = {};
				for(var k in node_json) {
					if(k == 'id' || k == 'topic' || k == 'children' || k == 'direction' || k == 'expanded') {
						continue;
					}
					data[k] = node_json[k];
				}
				return data;
			},

			_extract_subnode: function(mind, node_parent, node_json) {
				var df = jm.format.node_tree;
				var data = df._extract_data(node_json);
				var d = null;
				if(node_parent.isroot) {
					d = node_json.direction == 'left' ? jm.direction.left : jm.direction.right;
				}
				var node = mind.add_node(node_parent, node_json.id, node_json.topic, data, null, d, node_json.expanded);
				if('children' in node_json) {
					var children = node_json.children;
					for(var i = 0; i < children.length; i++) {
						df._extract_subnode(mind, node, children[i]);
					}
				}
			},

			_buildnode: function(node) {
				var df = jm.format.node_tree;
				if(!(node instanceof jm.node)) {
					return;
				}
				var o = {
					id: node.id,
					topic: node.topic,
					expanded: node.expanded
				};
				if(!!node.parent && node.parent.isroot) {
					o.direction = node.direction == jm.direction.left ? 'left' : 'right';
				}
				if(node.data != null) {
					var node_data = node.data;
					for(var k in node_data) {
						o[k] = node_data[k];
					}
				}
				var children = node.children;
				if(children.length > 0) {
					o.children = [];
					for(var i = 0; i < children.length; i++) {
						o.children.push(df._buildnode(children[i]));
					}
				}
				return o;
			}
		},

		node_array: {
			example: {
				"meta": {
					"name": __name__,
					"author": __author__,
					"version": __version__
				},
				"format": "node_array",
				"data": [{
					"id": "root",
					"topic": "jsMind Example",
					"isroot": true
				}]
			},

			get_mind: function(source) {
				var df = jm.format.node_array;
				var mind = new jm.mind();
				mind.name = source.meta.name;
				mind.author = source.meta.author;
				mind.version = source.meta.version;
				df._parse(mind, source.data);
				return mind;
			},

			get_data: function(mind) {
				var df = jm.format.node_array;
				var json = {};
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				};
				json.format = 'node_array';
				json.data = [];
				df._array(mind, json.data);
				return json;
			},

			_parse: function(mind, node_array) {
				var df = jm.format.node_array;
				var narray = node_array.slice(0);
				// reverse array for improving looping performance
				narray.reverse();
				var root_id = df._extract_root(mind, narray);
				if(!!root_id) {
					df._extract_subnode(mind, root_id, narray);
				} else {
					logger.error('root node can not be found');
				}
			},

			_extract_root: function(mind, node_array) {
				var df = jm.format.node_array;
				var i = node_array.length;
				while(i--) {
					if('isroot' in node_array[i] && node_array[i].isroot) {
						var root_json = node_array[i];
						var data = df._extract_data(root_json);
						mind.set_root(root_json.id, root_json.topic, data);
						node_array.splice(i, 1);
						return root_json.id;
					}
				}
				return null;
			},

			_extract_subnode: function(mind, parentid, node_array) {
				var df = jm.format.node_array;
				var i = node_array.length;
				var node_json = null;
				var data = null;
				var extract_count = 0;
				while(i--) {
					node_json = node_array[i];
					if(node_json.parentid == parentid) {
						data = df._extract_data(node_json);
						var d = null;
						var node_direction = node_json.direction;
						if(!!node_direction) {
							d = node_direction == 'left' ? jm.direction.left : jm.direction.right;
						}
						mind.add_node(parentid, node_json.id, node_json.topic, data, null, d, node_json.expanded);
						node_array.splice(i, 1);
						extract_count++;
						var sub_extract_count = df._extract_subnode(mind, node_json.id, node_array);
						if(sub_extract_count > 0) {
							// reset loop index after extract subordinate node
							i = node_array.length;
							extract_count += sub_extract_count;
						}
					}
				}
				return extract_count;
			},

			_extract_data: function(node_json) {
				var data = {};
				for(var k in node_json) {
					if(k == 'id' || k == 'topic' || k == 'parentid' || k == 'isroot' || k == 'direction' || k == 'expanded') {
						continue;
					}
					data[k] = node_json[k];
				}
				return data;
			},

			_array: function(mind, node_array) {
				var df = jm.format.node_array;
				df._array_node(mind.root, node_array);
			},

			_array_node: function(node, node_array) {
				var df = jm.format.node_array;
				if(!(node instanceof jm.node)) {
					return;
				}
				var o = {
					id: node.id,
					topic: node.topic,
					expanded: node.expanded
				};
				if(!!node.parent) {
					o.parentid = node.parent.id;
				}
				if(node.isroot) {
					o.isroot = true;
				}
				if(!!node.parent && node.parent.isroot) {
					o.direction = node.direction == jm.direction.left ? 'left' : 'right';
				}
				if(node.data != null) {
					var node_data = node.data;
					for(var k in node_data) {
						o[k] = node_data[k];
					}
				}
				node_array.push(o);
				var ci = node.children.length;
				for(var i = 0; i < ci; i++) {
					df._array_node(node.children[i], node_array);
				}
			},
		},

		freemind: {
			example: {
				"meta": {
					"name": __name__,
					"author": __author__,
					"version": __version__
				},
				"format": "freemind",
				"data": "<map version=\"1.0.1\"><node ID=\"root\" TEXT=\"freemind Example\"/></map>"
			},
			get_mind: function(source) {
				var df = jm.format.freemind;
				var mind = new jm.mind();
				mind.name = source.meta.name;
				mind.author = source.meta.author;
				mind.version = source.meta.version;
				var xml = source.data;
				var xml_doc = df._parse_xml(xml);
				var xml_root = df._find_root(xml_doc);
				df._load_node(mind, null, xml_root);
				return mind;
			},

			get_data: function(mind) {
				var df = jm.format.freemind;
				var json = {};
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				};
				json.format = 'freemind';
				var xmllines = [];
				xmllines.push('<map version=\"1.0.1\">');
				df._buildmap(mind.root, xmllines);
				xmllines.push('</map>');
				json.data = xmllines.join(' ');
				return json;
			},

			_parse_xml: function(xml) {
				var xml_doc = null;
				if(window.DOMParser) {
					var parser = new DOMParser();
					xml_doc = parser.parseFromString(xml, 'text/xml');
				} else { // Internet Explorer
					xml_doc = new ActiveXObject('Microsoft.XMLDOM');
					xml_doc.async = false;
					xml_doc.loadXML(xml);
				}
				return xml_doc;
			},

			_find_root: function(xml_doc) {
				var nodes = xml_doc.childNodes;
				var node = null;
				var root = null;
				var n = null;
				for(var i = 0; i < nodes.length; i++) {
					n = nodes[i];
					if(n.nodeType == 1 && n.tagName == 'map') {
						node = n;
						break;
					}
				}
				if(!!node) {
					var ns = node.childNodes;
					node = null;
					for(var i = 0; i < ns.length; i++) {
						n = ns[i];
						if(n.nodeType == 1 && n.tagName == 'node') {
							node = n;
							break;
						}
					}
				}
				return node;
			},

			_load_node: function(mind, parent_id, xml_node) {
				var df = jm.format.freemind;
				var node_id = xml_node.getAttribute('ID');
				var node_topic = xml_node.getAttribute('TEXT');
				// look for richcontent
				if(node_topic == null) {
					var topic_children = xml_node.childNodes;
					var topic_child = null;
					for(var i = 0; i < topic_children.length; i++) {
						topic_child = topic_children[i];
						//logger.debug(topic_child.tagName);
						if(topic_child.nodeType == 1 && topic_child.tagName === 'richcontent') {
							node_topic = topic_child.textContent;
							break;
						}
					}
				}
				var node_data = df._load_attributes(xml_node);
				var node_expanded = ('expanded' in node_data) ? (node_data.expanded == 'true') : true;
				delete node_data.expanded;

				var node_position = xml_node.getAttribute('POSITION');
				var node_direction = null;
				if(!!node_position) {
					node_direction = node_position == 'left' ? jm.direction.left : jm.direction.right;
				}
				//logger.debug(node_position +':'+ node_direction);
				if(!!parent_id) {
					mind.add_node(parent_id, node_id, node_topic, node_data, null, node_direction, node_expanded);
				} else {
					mind.set_root(node_id, node_topic, node_data);
				}
				var children = xml_node.childNodes;
				var child = null;
				for(var i = 0; i < children.length; i++) {
					child = children[i];
					if(child.nodeType == 1 && child.tagName == 'node') {
						df._load_node(mind, node_id, child);
					}
				}
			},

			_load_attributes: function(xml_node) {
				var children = xml_node.childNodes;
				var attr = null;
				var attr_data = {};
				for(var i = 0; i < children.length; i++) {
					attr = children[i];
					if(attr.nodeType == 1 && attr.tagName === 'attribute') {
						attr_data[attr.getAttribute('NAME')] = attr.getAttribute('VALUE');
					}
				}
				return attr_data;
			},

			_buildmap: function(node, xmllines) {
				var df = jm.format.freemind;
				var pos = null;
				if(!!node.parent && node.parent.isroot) {
					pos = node.direction === jm.direction.left ? 'left' : 'right';
				}
				xmllines.push('<node');
				xmllines.push('ID=\"' + node.id + '\"');
				if(!!pos) {
					xmllines.push('POSITION=\"' + pos + '\"');
				}
				xmllines.push('TEXT=\"' + node.topic + '\">');

				// store expanded status as an attribute
				xmllines.push('<attribute NAME=\"expanded\" VALUE=\"' + node.expanded + '\"/>');

				// for attributes
				var node_data = node.data;
				if(node_data != null) {
					for(var k in node_data) {
						xmllines.push('<attribute NAME=\"' + k + '\" VALUE=\"' + node_data[k] + '\"/>');
					}
				}

				// for children
				var children = node.children;
				for(var i = 0; i < children.length; i++) {
					df._buildmap(children[i], xmllines);
				}

				xmllines.push('</node>');
			},
		},
	};

	// ============= utility object =============================================

	jm.util = {
		is_node: function(node) {
			return !!node && node instanceof jm.node;
		},
		ajax: {
			_xhr: function() {
				var xhr = null;
				if(window.XMLHttpRequest) {
					xhr = new XMLHttpRequest();
				} else {
					try {
						xhr = new ActiveXObject('Microsoft.XMLHTTP');
					} catch(e) {}
				}
				return xhr;
			},
			_eurl: function(url) {
				return encodeURIComponent(url);
			},
			request: function(url, param, method, callback, fail_callback) {
				var a = jm.util.ajax;
				var p = null;
				var tmp_param = [];
				for(var k in param) {
					tmp_param.push(a._eurl(k) + '=' + a._eurl(param[k]));
				}
				if(tmp_param.length > 0) {
					p = tmp_param.join('&');
				}
				var xhr = a._xhr();
				if(!xhr) {
					return;
				}
				xhr.onreadystatechange = function() {
					if(xhr.readyState == 4) {
						if(xhr.status == 200 || xhr.status == 0) {
							if(typeof callback === 'function') {
								var data = jm.util.json.string2json(xhr.responseText);
								if(data != null) {
									callback(data);
								} else {
									callback(xhr.responseText);
								}
							}
						} else {
							if(typeof fail_callback === 'function') {
								fail_callback(xhr);
							} else {
								logger.error('xhr request failed.', xhr);
							}
						}
					}
				}
				method = method || 'GET';
				xhr.open(method, url, true);
				xhr.setRequestHeader('If-Modified-Since', '0');
				if(method == 'POST') {
					xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
					xhr.send(p);
				} else {
					xhr.send();
				}
			},
			get: function(url, callback) {
				return jm.util.ajax.request(url, {}, 'GET', callback);
			},
			post: function(url, param, callback) {
				return jm.util.ajax.request(url, param, 'POST', callback);
			}
		},

		dom: {
			//target,eventType,handler
			add_event: function(t, e, h) {
				if(!!t.addEventListener) {
					t.addEventListener(e, h, false);
				} else {
					t.attachEvent('on' + e, h);
				}
			}
		},

		canvas: {
			bezierto: function(ctx, x1, y1, x2, y2) {
				ctx.beginPath();
				ctx.moveTo(x1, y1);
				ctx.bezierCurveTo(x1 + (x2 - x1) * 2 / 3, y1, x1, y2, x2, y2);
				ctx.stroke();
			},
			lineto: function(ctx, x1, y1, x2, y2) {
				ctx.beginPath();
				ctx.moveTo(x1, y1);
				ctx.lineTo(x2, y2);
				ctx.stroke();
			},
			clear: function(ctx, x, y, w, h) {
				ctx.clearRect(x, y, w, h);
			}
		},

		file: {
			read: function(file_data, fn_callback) {
				var reader = new FileReader();
				reader.onload = function() {
					if(typeof fn_callback === 'function') {
						fn_callback(this.result, file_data.name);
					}
				};
				reader.readAsText(file_data);
			},

			save: function(file_data, type, name) {
				var blob;
				if(typeof $w.Blob === 'function') {
					blob = new Blob([file_data], {
						type: type
					});
				} else {
					var BlobBuilder = $w.BlobBuilder || $w.MozBlobBuilder || $w.WebKitBlobBuilder || $w.MSBlobBuilder;
					var bb = new BlobBuilder();
					bb.append(file_data);
					blob = bb.getBlob(type);
				}
				if(navigator.msSaveBlob) {
					navigator.msSaveBlob(blob, name);
				} else {
					var URL = $w.URL || $w.webkitURL;
					var bloburl = URL.createObjectURL(blob);
					var anchor = $c('a');
					if('download' in anchor) {
						anchor.style.visibility = 'hidden';
						anchor.href = bloburl;
						anchor.download = name;
						$d.body.appendChild(anchor);
						var evt = $d.createEvent('MouseEvents');
						evt.initEvent('click', true, true);
						anchor.dispatchEvent(evt);
						$d.body.removeChild(anchor);
					} else {
						location.href = bloburl;
					}
				}
			}
		},

		json: {
			json2string: function(json) {
				if(!!JSON) {
					try {
						var json_str = JSON.stringify(json);
						return json_str;
					} catch(e) {
						logger.warn(e);
						logger.warn('can not convert to string');
						return null;
					}
				}
			},
			string2json: function(json_str) {
				if(!!JSON) {
					try {
						var json = JSON.parse(json_str);
						return json;
					} catch(e) {
						logger.warn(e);
						logger.warn('can not parse to json');
						return null;
					}
				}
			},
			merge: function(b, a) {
				for(var o in a) {
					if(o in b) {
						if(typeof b[o] === 'object' &&
							Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
							!b[o].length) {
							jm.util.json.merge(b[o], a[o]);
						} else {
							b[o] = a[o];
						}
					} else {
						b[o] = a[o];
					}
				}
				return b;
			}
		},

		uuid: {
			newid: function() {
				return(new Date().getTime().toString(16) + Math.random().toString(16).substr(2)).substr(2, 16);
			}
		},

		text: {
			is_empty: function(s) {
				if(!s) {
					return true;
				}
				return s.replace(/\s*/, '').length == 0;
			}
		}
	};

	jm.prototype = {
		init: function() {
			if(this.inited) {
				return;
			}
			this.inited = true;

			var opts = this.options;

			var opts_layout = {
				mode: opts.mode,
				hspace: opts.layout.hspace,
				vspace: opts.layout.vspace,
				pspace: opts.layout.pspace
			}
			var opts_view = {
				container: opts.container,
				support_html: opts.support_html,
				hmargin: opts.view.hmargin,
				vmargin: opts.view.vmargin,
				line_width: opts.view.line_width,
				line_color: opts.view.line_color
			};
			// create instance of function provider
			this.data = new jm.data_provider(this);
			this.layout = new jm.layout_provider(this, opts_layout);
			this.view = new jm.view_provider(this, opts_view);
			this.shortcut = new jm.shortcut_provider(this, opts.shortcut);

			this.data.init();
			this.layout.init();
			this.view.init();
			this.shortcut.init();

			this._event_bind();

			jm.init_plugins(this);
		},

		enable_edit: function() {
			this.options.editable = true;
		},

		disable_edit: function() {
			this.options.editable = false;
		},

		// call enable_event_handle('dblclick')
		// options are 'mousedown', 'click', 'dblclick'
		enable_event_handle: function(event_handle) {
			this.options.default_event_handle['enable_' + event_handle + '_handle'] = true;
		},

		// call disable_event_handle('dblclick')
		// options are 'mousedown', 'click', 'dblclick'
		disable_event_handle: function(event_handle) {
			this.options.default_event_handle['enable_' + event_handle + '_handle'] = false;
		},

		get_editable: function() {
			return this.options.editable;
		},

		set_theme: function(theme) {
			var theme_old = this.options.theme;
			this.options.theme = (!!theme) ? theme : null;
			if(theme_old != this.options.theme) {
				this.view.reset_theme();
				this.view.reset_custom_style();
			}
		},
		_event_bind: function() {
			this.view.add_event(this, 'mousedown', this.mousedown_handle);
			this.view.add_event(this, 'click', this.click_handle);
			this.view.add_event(this, 'dblclick', this.dblclick_handle);
		},

		mousedown_handle: function(e) { 
			if(!this.options.default_event_handle['enable_mousedown_handle']) {
				return;
			}
			var element = e.target || event.srcElement;		
			var nodeid = this.view.get_binded_nodeid(element);
			if(!!nodeid) {
				this.select_node(nodeid);
			} else {
				this.select_clear();
			}
		},

		click_handle: function(e) {
			if(!this.options.default_event_handle['enable_click_handle']) {
				return;
			}
			var element = e.target || event.srcElement;
			var isexpander = this.view.is_expander(element);
			if(isexpander) {
				var nodeid = this.view.get_binded_nodeid(element);
				if(!!nodeid) {
					this.toggle_node(nodeid);
				}
			}
		},

		dblclick_handle: function(e) {
			if(!this.options.default_event_handle['enable_dblclick_handle']) {
				return;
			}
			if(this.get_editable()) {
				var element = e.target || event.srcElement;
				var nodeid = this.view.get_binded_nodeid(element);
				if(!!nodeid) {
					this.begin_edit(nodeid);
				}
			}
		},

		begin_edit: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return false;
				} else {
					return this.begin_edit(the_node);
				}
			}
			if(this.get_editable()) {
				this.view.edit_node_begin(node);
			} else {
				logger.error('fail, this mind map is not editable.');
				return;
			}
		},

		end_edit: function() {
			this.view.edit_node_end();
		},

		toggle_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.toggle_node(the_node);
				}
			}
			if(node.isroot) {
				return;
			}
			this.view.save_location(node);
			this.layout.toggle_node(node);
			this.view.relayout();
			this.view.restore_location(node);
		},

		expand_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.expand_node(the_node);
				}
			}
			if(node.isroot) {
				return;
			}
			this.view.save_location(node);
			this.layout.expand_node(node);
			this.view.relayout();
			this.view.restore_location(node);
		},

		collapse_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.collapse_node(the_node);
				}
			}
			if(node.isroot) {
				return;
			}
			this.view.save_location(node);
			this.layout.collapse_node(node);
			this.view.relayout();
			this.view.restore_location(node);
		},

		expand_all: function() {
			this.layout.expand_all();
			this.view.relayout();
		},

		collapse_all: function() {
			this.layout.collapse_all();
			this.view.relayout();
		},

		expand_to_depth: function(depth) {
			this.layout.expand_to_depth(depth);
			this.view.relayout();
		},

		_reset: function() {
			this.view.reset();
			this.layout.reset();
			this.data.reset();
		},

		_show: function(mind) {
			var m = mind || jm.format.node_array.example;

			this.mind = this.data.load(m);
			if(!this.mind) {
				logger.error('data.load error');
				return;
			} else {
				logger.debug('data.load ok');
			}

			this.view.load();
			logger.debug('view.load ok');

			this.layout.layout();
			logger.debug('layout.layout ok');

			this.view.show(true);
			logger.debug('view.show ok');

			this.invoke_event_handle(jm.event_type.show, {
				data: [mind]
			});
		},

		show: function(mind) {
			this._reset();
			this._show(mind);
		},

		get_meta: function() {
			return {
				name: this.mind.name,
				author: this.mind.author,
				version: this.mind.version
			};
		},

		get_data: function(data_format) {
			var df = data_format || 'node_tree';
			return this.data.get_data(df);
		},

		get_root: function() {
			return this.mind.root;
		},

		get_node: function(nodeid) {
			return this.mind.get_node(nodeid);
		},

		add_node: function(parent_node, nodeid, topic, data) {
			if(this.get_editable()) {
				var node = this.mind.add_node(parent_node, nodeid, topic, data);
				if(!!node) {
					this.view.add_node(node);
					this.layout.layout();
					this.view.show(false);
					this.view.reset_node_custom_style(node);
					this.expand_node(parent_node);
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'add_node',
						data: [parent_node.id, nodeid, topic, data],
						node: nodeid
					});
				}
				return node;
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		insert_node_before: function(node_before, nodeid, topic, data) {
			if(this.get_editable()) {
				var beforeid = jm.util.is_node(node_before) ? node_before.id : node_before;
				var node = this.mind.insert_node_before(node_before, nodeid, topic, data);
				if(!!node) {
					this.view.add_node(node);
					this.layout.layout();
					this.view.show(false);
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'insert_node_before',
						data: [beforeid, nodeid, topic, data],
						node: nodeid
					});
				}
				return node;
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		insert_node_after: function(node_after, nodeid, topic, data) {
			if(this.get_editable()) {
				var afterid = jm.util.is_node(node_after) ? node_after.id : node_after;
				var node = this.mind.insert_node_after(node_after, nodeid, topic, data);
				if(!!node) {
					this.view.add_node(node);
					this.layout.layout();
					this.view.show(false);
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'insert_node_after',
						data: [afterid, nodeid, topic, data],
						node: nodeid
					});
				}
				return node;
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		remove_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return false;
				} else {
					return this.remove_node(the_node);
				}
			}
			if(this.get_editable()) {
				if(node.isroot) {
					logger.error('fail, can not remove root node');
					return false;
				}
				var nodeid = node.id;
				var parentid = node.parent.id;
				var parent_node = this.get_node(parentid);
				this.view.save_location(parent_node);
				this.view.remove_node(node);
				this.mind.remove_node(node);
				this.layout.layout();
				this.view.show(false);
				this.view.restore_location(parent_node);
				this.invoke_event_handle(jm.event_type.edit, {
					evt: 'remove_node',
					data: [nodeid],
					node: parentid
				});
				return true;
			} else {
				logger.error('fail, this mind map is not editable');
				return false;
			}
		},

		update_node: function(nodeid, topic) {
			if(this.get_editable()) {
				if(jm.util.text.is_empty(topic)) {
					logger.warn('fail, topic can not be empty');
					return;
				}
				var node = this.get_node(nodeid);
				if(!!node) {
					if(node.topic === topic) {
						logger.info('nothing changed');
						this.view.update_node(node);
						return;
					}
					node.topic = topic;
					this.view.update_node(node);
					this.layout.layout();
					this.view.show(false);
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'update_node',
						data: [nodeid, topic],
						node: nodeid
					});
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return;
			}
		},

		move_node: function(nodeid, beforeid, parentid, direction) { 
			if(this.get_editable()) {
				var node = this.mind.move_node(nodeid, beforeid, parentid, direction);
				if(!!node) {
					this.view.update_node(node);
					this.layout.layout();
					this.view.show(false);
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'move_node',
						data: [nodeid, beforeid, parentid, direction],
						node: nodeid
					});
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return;
			}
		},

		select_node: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.select_node(the_node);
				}
			}
			if(!this.layout.is_visible(node)) {
				return;
			}
			this.mind.selected = node;
			this.view.select_node(node);
		},

		get_selected_node: function() {
			if(!!this.mind) {
				return this.mind.selected;
			} else {
				return null;
			}
		},

		select_clear: function() {
			if(!!this.mind) {
				this.mind.selected = null;
				this.view.select_clear();
			}
		},

		is_node_visible: function(node) {
			return this.layout.is_visible(node);
		},

		find_node_before: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.find_node_before(the_node);
				}
			}
			if(node.isroot) {
				return null;
			}
			var n = null;
			if(node.parent.isroot) {
				var c = node.parent.children;
				var prev = null;
				var ni = null;
				for(var i = 0; i < c.length; i++) {
					ni = c[i];
					if(node.direction === ni.direction) {
						if(node.id === ni.id) {
							n = prev;
						}
						prev = ni;
					}
				}
			} else {
				n = this.mind.get_node_before(node);
			}
			return n;
		},

		find_node_after: function(node) {
			if(!jm.util.is_node(node)) {
				var the_node = this.get_node(node);
				if(!the_node) {
					logger.error('the node[id=' + node + '] can not be found.');
					return;
				} else {
					return this.find_node_after(the_node);
				}
			}
			if(node.isroot) {
				return null;
			}
			var n = null;
			if(node.parent.isroot) {
				var c = node.parent.children;
				var getthis = false;
				var ni = null;
				for(var i = 0; i < c.length; i++) {
					ni = c[i];
					if(node.direction === ni.direction) {
						if(getthis) {
							n = ni;
							break;
						}
						if(node.id === ni.id) {
							getthis = true;
						}
					}
				}
			} else {
				n = this.mind.get_node_after(node);
			}
			return n;
		},

		set_node_color: function(nodeid, bgcolor, fgcolor) {
			if(this.get_editable()) {
				var node = this.mind.get_node(nodeid);
				if(!!node) {
					if(!!bgcolor) {
						node.data['background-color'] = bgcolor;
					}
					if(!!fgcolor) {
						node.data['foreground-color'] = fgcolor;
					}
					this.view.reset_node_custom_style(node);
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		set_node_font_style: function(nodeid, size, weight, style) {
			if(this.get_editable()) {
				var node = this.mind.get_node(nodeid);
				if(!!node) {
					if(!!size) {
						node.data['font-size'] = size;
					}
					if(!!weight) {
						node.data['font-weight'] = weight;
					}
					if(!!style) {
						node.data['font-style'] = style;
					}
					this.view.reset_node_custom_style(node);
					this.view.update_node(node);
					this.layout.layout();
					this.view.show(false);
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		set_node_background_image: function(nodeid, image, width, height, rotation) {
			if(this.get_editable()) {
				var node = this.mind.get_node(nodeid);
				if(!!node) {
					if(!!image) {
						node.data['background-image'] = image;
					}
					if(!!width) {
						node.data['width'] = width;
					}
					if(!!height) {
						node.data['height'] = height;
					}
					if(!!rotation) {
						node.data['background-rotation'] = rotation;
					}
					this.view.reset_node_custom_style(node);
					this.view.update_node(node);
					this.layout.layout();
					this.view.show(false);
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		set_node_background_rotation: function(nodeid, rotation) {
			if(this.get_editable()) {
				var node = this.mind.get_node(nodeid);
				if(!!node) {
					if(!node.data['background-image']) {
						logger.error('fail, only can change rotation angle of node with background image');
						return null;
					}
					node.data['background-rotation'] = rotation;
					this.view.reset_node_custom_style(node);
					this.view.update_node(node);
					this.layout.layout();
					this.view.show(false);
				}
			} else {
				logger.error('fail, this mind map is not editable');
				return null;
			}
		},

		resize: function() {
			this.view.resize();
		},

		// callback(type ,data)
		add_event_listener: function(callback) {
			if(typeof callback === 'function') {
				this.event_handles.push(callback);
			}
		},

		invoke_event_handle: function(type, data) {
			var j = this;
			$w.setTimeout(function() {
				j._invoke_event_handle(type, data);
			}, 0);

			
		},

		_invoke_event_handle: function(type, data) {
			var l = this.event_handles.length;
			for(var i = 0; i < l; i++) {
				this.event_handles[i](type, data);
			}
		}

	};

	// ============= data provider =============================================

	jm.data_provider = function(jm) {
		this.jm = jm;
	};

	jm.data_provider.prototype = {
		init: function() {
			logger.debug('data.init');
		},

		reset: function() {
			logger.debug('data.reset');
		},

		load: function(mind_data) {
			var df = null;
			var mind = null;
			if(typeof mind_data === 'object') {
				if(!!mind_data.format) {
					df = mind_data.format;
				} else {
					df = 'node_tree';
				}
			} else {
				df = 'freemind';
			}

			if(df == 'node_array') {
				mind = jm.format.node_array.get_mind(mind_data);
			} else if(df == 'node_tree') {
				mind = jm.format.node_tree.get_mind(mind_data);
			} else if(df == 'freemind') {
				mind = jm.format.freemind.get_mind(mind_data);
			} else {
				logger.warn('unsupported format');
			}
			return mind;
		},

		get_data: function(data_format) {
			var data = null;
			if(data_format == 'node_array') {
				data = jm.format.node_array.get_data(this.jm.mind);
			} else if(data_format == 'node_tree') {
				data = jm.format.node_tree.get_data(this.jm.mind);
			} else if(data_format == 'freemind') {
				data = jm.format.freemind.get_data(this.jm.mind);
			} else {
				logger.error('unsupported ' + data_format + ' format');
			}
			return data;
		},
	};

	// ============= layout provider ===========================================

	jm.layout_provider = function(jm, options) {
		this.opts = options;
		this.jm = jm;
		this.isside = (this.opts.mode == 'side');
		this.bounds = null;

		this.cache_valid = false;
	};

	jm.layout_provider.prototype = {
		init: function() {
			logger.debug('layout.init');
		},
		reset: function() {
			logger.debug('layout.reset');
			this.bounds = {
				n: 0,
				s: 0,
				w: 0,
				e: 0
			};
		},
		layout: function() {
			logger.debug('layout.layout');
			this.layout_direction();
			this.layout_offset();
		},

		layout_direction: function() {
			this._layout_direction_root();
		},

		_layout_direction_root: function() {
			var node = this.jm.mind.root;
			// logger.debug(node);
			var layout_data = null;
			if('layout' in node._data) {
				layout_data = node._data.layout;
			} else {
				layout_data = {};
				node._data.layout = layout_data;
			}
			var children = node.children;
			var children_count = children.length;
			layout_data.direction = jm.direction.center;
			layout_data.side_index = 0;
			if(this.isside) {
				var i = children_count;
				while(i--) {
					this._layout_direction_side(children[i], jm.direction.right, i);
				}
			} else {
				var i = children_count;
				var subnode = null;
				while(i--) {
					subnode = children[i];
					if(subnode.direction == jm.direction.left) {
						this._layout_direction_side(subnode, jm.direction.left, i);
					} else {
						this._layout_direction_side(subnode, jm.direction.right, i);
					}
				}
				/*
				var boundary = Math.ceil(children_count/2);
				var i = children_count;
				while(i--){
				    if(i>=boundary){
				        this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
				    }else{
				        this._layout_direction_side(children[i],jm.direction.right, i);
				    }
				}*/

			}
		},

		_layout_direction_side: function(node, direction, side_index) {
			var layout_data = null;
			if('layout' in node._data) {
				layout_data = node._data.layout;
			} else {
				layout_data = {};
				node._data.layout = layout_data;
			}
			var children = node.children;
			var children_count = children.length;

			layout_data.direction = direction;
			layout_data.side_index = side_index;
			var i = children_count;
			while(i--) {
				this._layout_direction_side(children[i], direction, i);
			}
		},

		layout_offset: function() {
			var node = this.jm.mind.root;
			var layout_data = node._data.layout;
			layout_data.offset_x = 0;
			layout_data.offset_y = 0;
			layout_data.outer_height = 0;
			var children = node.children;
			var i = children.length;
			var left_nodes = [];
			var right_nodes = [];
			var subnode = null;
			while(i--) {
				subnode = children[i];
				if(subnode._data.layout.direction == jm.direction.right) {
					right_nodes.unshift(subnode);
				} else {
					left_nodes.unshift(subnode);
				}
			}
			layout_data.left_nodes = left_nodes;
			layout_data.right_nodes = right_nodes;
			layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes);
			layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes);
			this.bounds.e = node._data.view.width / 2;
			this.bounds.w = 0 - this.bounds.e;
			//logger.debug(this.bounds.w);
			this.bounds.n = 0;
			this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right);
		},

		// layout both the x and y axis
		_layout_offset_subnodes: function(nodes) {
			var total_height = 0;
			var nodes_count = nodes.length;
			var i = nodes_count;
			var node = null;
			var node_outer_height = 0;
			var layout_data = null;
			var base_y = 0;
			var pd = null; // parent._data
			while(i--) {
				node = nodes[i];
				layout_data = node._data.layout;
				if(pd == null) {
					pd = node.parent._data;
				}

				node_outer_height = this._layout_offset_subnodes(node.children);
				if(!node.expanded) {
					node_outer_height = 0;
					this.set_visible(node.children, false);
				}
				node_outer_height = Math.max(node._data.view.height, node_outer_height);

				layout_data.outer_height = node_outer_height;
				layout_data.offset_y = base_y - node_outer_height / 2;
				layout_data.offset_x = this.opts.hspace * layout_data.direction + pd.view.width * (pd.layout.direction + layout_data.direction) / 2;
				if(!node.parent.isroot) {
					layout_data.offset_x += this.opts.pspace * layout_data.direction;
				}

				base_y = base_y - node_outer_height - this.opts.vspace;
				total_height += node_outer_height;
			}
			if(nodes_count > 1) {
				total_height += this.opts.vspace * (nodes_count - 1);
			}
			i = nodes_count;
			var middle_height = total_height / 2;
			while(i--) {
				node = nodes[i];
				node._data.layout.offset_y += middle_height;
			}
			return total_height;
		},

		// layout the y axis only, for collapse/expand a node
		_layout_offset_subnodes_height: function(nodes) {
			var total_height = 0;
			var nodes_count = nodes.length;
			var i = nodes_count;
			var node = null;
			var node_outer_height = 0;
			var layout_data = null;
			var base_y = 0;
			var pd = null; // parent._data
			while(i--) {
				node = nodes[i];
				layout_data = node._data.layout;
				if(pd == null) {
					pd = node.parent._data;
				}

				node_outer_height = this._layout_offset_subnodes_height(node.children);
				if(!node.expanded) {
					node_outer_height = 0;
				}
				node_outer_height = Math.max(node._data.view.height, node_outer_height);

				layout_data.outer_height = node_outer_height;
				layout_data.offset_y = base_y - node_outer_height / 2;
				base_y = base_y - node_outer_height - this.opts.vspace;
				total_height += node_outer_height;
			}
			if(nodes_count > 1) {
				total_height += this.opts.vspace * (nodes_count - 1);
			}
			i = nodes_count;
			var middle_height = total_height / 2;
			while(i--) {
				node = nodes[i];
				node._data.layout.offset_y += middle_height;
				//logger.debug(node.topic);
				//logger.debug(node._data.layout.offset_y);
			}
			return total_height;
		},

		get_node_offset: function(node) {
			var layout_data = node._data.layout;
			var offset_cache = null;
			if(('_offset_' in layout_data) && this.cache_valid) {
				offset_cache = layout_data._offset_;
			} else {
				offset_cache = {
					x: -1,
					y: -1
				};
				layout_data._offset_ = offset_cache;
			}
			if(offset_cache.x == -1 || offset_cache.y == -1) {
				var x = layout_data.offset_x;
				var y = layout_data.offset_y;
				if(!node.isroot) {
					var offset_p = this.get_node_offset(node.parent);
					x += offset_p.x;
					y += offset_p.y;
				}
				offset_cache.x = x;
				offset_cache.y = y;
			}
			return offset_cache;
		},

		get_node_point: function(node) {
			var view_data = node._data.view;
			var offset_p = this.get_node_offset(node);
			//logger.debug(offset_p);
			var p = {};
			p.x = offset_p.x + view_data.width * (node._data.layout.direction - 1) / 2;
			p.y = offset_p.y - view_data.height / 2;
			//logger.debug(p);
			return p;
		},

		get_node_point_in: function(node) {
			var p = this.get_node_offset(node);
			return p;
		},

		get_node_point_out: function(node) {
			var layout_data = node._data.layout;
			var pout_cache = null;
			if(('_pout_' in layout_data) && this.cache_valid) {
				pout_cache = layout_data._pout_;
			} else {
				pout_cache = {
					x: -1,
					y: -1
				};
				layout_data._pout_ = pout_cache;
			}
			if(pout_cache.x == -1 || pout_cache.y == -1) {
				if(node.isroot) {
					pout_cache.x = 0;
					pout_cache.y = 0;
				} else {
					var view_data = node._data.view;
					var offset_p = this.get_node_offset(node);
					pout_cache.x = offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction;
					pout_cache.y = offset_p.y;
					//logger.debug('pout');
					//logger.debug(pout_cache);
				}
			}
			return pout_cache;
		},

		get_expander_point: function(node) {
			var p = this.get_node_point_out(node);
			var ex_p = {};
			if(node._data.layout.direction == jm.direction.right) {
				ex_p.x = p.x - this.opts.pspace;
			} else {
				ex_p.x = p.x;
			}
			ex_p.y = p.y - Math.ceil(this.opts.pspace / 2);
			return ex_p;
		},

		get_min_size: function() {
			var nodes = this.jm.mind.nodes;
			var node = null;
			var pout = null;
			for(var nodeid in nodes) {
				node = nodes[nodeid];
				pout = this.get_node_point_out(node);
				//logger.debug(pout.x);
				if(pout.x > this.bounds.e) {
					this.bounds.e = pout.x;
				}
				if(pout.x < this.bounds.w) {
					this.bounds.w = pout.x;
				}
			}
			return {
				w: this.bounds.e - this.bounds.w,
				h: this.bounds.s - this.bounds.n
			}
		},

		toggle_node: function(node) {
			if(node.isroot) {
				return;
			}
			if(node.expanded) {
				this.collapse_node(node);
			} else {
				this.expand_node(node);
			}
		},

		expand_node: function(node) {
			node.expanded = true;
			this.part_layout(node);
			this.set_visible(node.children, true);
		},

		collapse_node: function(node) {
			node.expanded = false;
			this.part_layout(node);
			this.set_visible(node.children, false);
		},

		expand_all: function() {
			var nodes = this.jm.mind.nodes;
			var c = 0;
			var node;
			for(var nodeid in nodes) {
				node = nodes[nodeid];
				if(!node.expanded) {
					node.expanded = true;
					c++;
				}
			}
			if(c > 0) {
				var root = this.jm.mind.root;
				this.part_layout(root);
				this.set_visible(root.children, true);
			}
		},

		collapse_all: function() {
			var nodes = this.jm.mind.nodes;
			var c = 0;
			var node;
			for(var nodeid in nodes) {
				node = nodes[nodeid];
				if(node.expanded && !node.isroot) {
					node.expanded = false
					c++;
				}
			}
			if(c > 0) {
				var root = this.jm.mind.root;
				this.part_layout(root);
				this.set_visible(root.children, true);
			}
		},

		expand_to_depth: function(target_depth, curr_nodes, curr_depth) {
			if(target_depth < 1) {
				return;
			}
			var nodes = curr_nodes || this.jm.mind.root.children;
			var depth = curr_depth || 1;
			var i = nodes.length;
			var node = null;
			while(i--) {
				node = nodes[i];
				if(depth < target_depth) {
					if(!node.expanded) {
						this.expand_node(node);
					}
					this.expand_to_depth(target_depth, node.children, depth + 1);
				}
				if(depth == target_depth) {
					if(node.expanded) {
						this.collapse_node(node);
					}
				}
			}
		},

		part_layout: function(node) {
			var root = this.jm.mind.root;
			if(!!root) {
				var root_layout_data = root._data.layout;
				if(node.isroot) {
					root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
					root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
				} else {
					if(node._data.layout.direction == jm.direction.right) {
						root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
					} else {
						root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
					}
				}
				this.bounds.s = Math.max(root_layout_data.outer_height_left, root_layout_data.outer_height_right);
				this.cache_valid = false;
			} else {
				logger.warn('can not found root node');
			}
		},

		set_visible: function(nodes, visible) {
			var i = nodes.length;
			var node = null;
			var layout_data = null;
			while(i--) {
				node = nodes[i];
				layout_data = node._data.layout;
				if(node.expanded) {
					this.set_visible(node.children, visible);
				} else {
					this.set_visible(node.children, false);
				}
				if(!node.isroot) {
					node._data.layout.visible = visible;
				}
			}
		},

		is_expand: function(node) {
			return node.expanded;
		},

		is_visible: function(node) {
			var layout_data = node._data.layout;
			if(('visible' in layout_data) && !layout_data.visible) {
				return false;
			} else {
				return true;
			}
		},
	};

	// view provider
	jm.view_provider = function(jm, options) {
		this.opts = options;
		this.jm = jm;
		this.layout = jm.layout;

		this.container = null;
		this.e_panel = null;
		this.e_nodes = null;
		this.e_canvas = null;

		this.canvas_ctx = null;
		this.size = {
			w: 0,
			h: 0
		};

		this.selected_node = null;
		this.editing_node = null;
	};

	jm.view_provider.prototype = {
		init: function() {
			logger.debug('view.init');

			this.container = $i(this.opts.container) ? this.opts.container : $g(this.opts.container);
			if(!this.container) {
				logger.error('the options.view.container was not be found in dom');
				return;
			}
			this.e_panel = $c('div');
			this.e_canvas = $c('canvas');
			this.e_nodes = $c('jmnodes');
			this.e_editor = $c('input');

			this.e_panel.className = 'jsmind-inner';
			this.e_panel.appendChild(this.e_canvas);
			this.e_panel.appendChild(this.e_nodes);

			this.e_editor.className = 'jsmind-editor';
			this.e_editor.type = 'text';

			this.actualZoom = 1;
			this.zoomStep = 0.1;
			this.minZoom = 0.5;
			this.maxZoom = 2;

			var v = this;
			jm.util.dom.add_event(this.e_editor, 'keydown', function(e) {
				var evt = e || event;
				if(evt.keyCode == 13) {
					v.edit_node_end();
					evt.stopPropagation();
				}
			});
			jm.util.dom.add_event(this.e_editor, 'blur', function(e) {
				v.edit_node_end();
			});

			this.container.appendChild(this.e_panel);

			this.init_canvas();
		},

		add_event: function(obj, event_name, event_handle) {
			jm.util.dom.add_event(this.e_nodes, event_name, function(e) {
				var evt = e || event;
				event_handle.call(obj, evt);
			});
		},

		get_binded_nodeid: function(element) {
			if(element == null) {
				return null;
			}
			var tagName = element.tagName.toLowerCase();
			if(tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
				return null;
			}
			if(tagName == 'jmnode' || tagName == 'jmexpander') {
				return element.getAttribute('nodeid');
			} else {
				return this.get_binded_nodeid(element.parentElement);
			}
		},

		is_expander: function(element) {
			return(element.tagName.toLowerCase() == 'jmexpander');
		},

		reset: function() {
			logger.debug('view.reset');
			this.selected_node = null;
			this.clear_lines();
			this.clear_nodes();
			this.reset_theme();
		},

		reset_theme: function() {
			var theme_name = this.jm.options.theme;
			if(!!theme_name) {
				this.e_nodes.className = 'theme-' + theme_name;
			} else {
				this.e_nodes.className = '';
			}
		},

		reset_custom_style: function() {
			var nodes = this.jm.mind.nodes;
			for(var nodeid in nodes) {
				this.reset_node_custom_style(nodes[nodeid]);
			}
		},

		load: function() {
			logger.debug('view.load');
			this.init_nodes();
		},

		expand_size: function() {
			var min_size = this.layout.get_min_size();
			var min_width = min_size.w + this.opts.hmargin * 2;
			var min_height = min_size.h + this.opts.vmargin * 2;
			var client_w = this.e_panel.clientWidth;
			var client_h = this.e_panel.clientHeight;
			if(client_w < min_width) {
				client_w = min_width;
			}
			if(client_h < min_height) {
				client_h = min_height;
			}
			this.size.w = client_w;
			this.size.h = client_h;
		},

		init_canvas: function() {
			var ctx = this.e_canvas.getContext('2d');
			this.canvas_ctx = ctx;
		},

		init_nodes_size: function(node) {
			var view_data = node._data.view;
			view_data.width = view_data.element.clientWidth;
			view_data.height = view_data.element.clientHeight;
		},

		init_nodes: function() {
			var nodes = this.jm.mind.nodes;
			var doc_frag = $d.createDocumentFragment();
			for(var nodeid in nodes) {
				this.create_node_element(nodes[nodeid], doc_frag);
			}
			this.e_nodes.appendChild(doc_frag);
			for(var nodeid in nodes) {
				this.init_nodes_size(nodes[nodeid]);
			}
		},

		add_node: function(node) {
			this.create_node_element(node, this.e_nodes);
			this.init_nodes_size(node);
		},

		create_node_element: function(node, parent_node) {
			var view_data = null;
			if('view' in node._data) {
				view_data = node._data.view;
			} else {
				view_data = {};
				node._data.view = view_data;
			}

			var d = $c('jmnode');
			if(node.isroot) {
				d.className = 'root';
			} else {
				var d_e = $c('jmexpander');
				$t(d_e, '-');
				d_e.setAttribute('nodeid', node.id);
				d_e.style.visibility = 'hidden';
				parent_node.appendChild(d_e);
				view_data.expander = d_e;
			}
			if(!!node.topic) {
				if(this.opts.support_html) {
					// $h(d,node.topic);	

					var markNodeHeight = 0; //备注和附件标志容器高度
					var remarkNode = ''; //备注标记
					var filemarkNode = ''; //附件标记
					if(node.data.remarkText != null && node.data.remarkText != undefined && node.data.remarkText != '') {
						//备注
						remarkNode = "<remark class='remarksN' nodeid='rmk_" + node.id + "' title='" + node.data.remarkText + "'></remark>";
					}
					if(node.data.fileInfo != null && node.data.fileInfo != undefined && node.data.fileInfo != '') {
						//附件
						filemarkNode = "<filemark class='fileMarkN' nodeid='fmk_" + node.id + "' title='" + node.data.fileInfo + "'></filemark>";
					}
					if(remarkNode != '' || filemarkNode != '') {
						//备注和附件父容器的高度
						markNodeHeight = 20;
					}

					//创建节点
					d.innerHTML = "<div class='mindMarkBox' style='height:" + markNodeHeight + "px'>" + remarkNode + filemarkNode + "</div>" +
						"<p class='nodeContent'>" + node.topic + "</p>";

				} else {
					$t(d, node.topic);
				}
			}
			d.setAttribute('nodeid', node.id);
			d.style.visibility = 'hidden';
			this._reset_node_custom_style(d, node.data);
			parent_node.appendChild(d);
			view_data.element = d;
		},

		remove_node: function(node) {
			if(this.selected_node != null && this.selected_node.id == node.id) {
				this.selected_node = null;
			}
			if(this.editing_node != null && this.editing_node.id == node.id) {
				node._data.view.element.removeChild(this.e_editor);
				this.editing_node = null;
			}
			var children = node.children;
			var i = children.length;
			while(i--) {
				this.remove_node(children[i]);
			}
			if(node._data.view) {
				var element = node._data.view.element;
				var expander = node._data.view.expander;
				this.e_nodes.removeChild(element);
				this.e_nodes.removeChild(expander);
				node._data.view.element = null;
				node._data.view.expander = null;
			}
		},

		update_node: function(node) {
			var view_data = node._data.view;
			var element = view_data.element;
			if(!!node.topic) {
				if(this.opts.support_html) {
					//                  $h(element,node.topic);
					$h(element.children[1], node.topic);
				} else {
					//                  $t(element,node.topic);
					$h(element.children[1], node.topic);
				}
			}
			view_data.width = element.clientWidth;
			view_data.height = element.clientHeight;
		},

		select_node: function(node) {
			if(!!this.selected_node) {
				this.selected_node._data.view.element.className =
					this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '');
				this.reset_node_custom_style(this.selected_node);
			}
			if(!!node) {
				this.selected_node = node;
				node._data.view.element.className += ' selected';
				this.clear_node_custom_style(node);
				thisNodeData = node;
			}
		},

		select_clear: function() {
			this.select_node(null);
		},

		get_editing_node: function() {
			return this.editing_node;
		},

		is_editing: function() {
			return(!!this.editing_node);
		},

		edit_node_begin: function(node) {
			if(!node.topic) {
				logger.warn("don't edit image nodes");
				return;
			}
			if(this.editing_node != null) {
				this.edit_node_end();
			}
			this.editing_node = node;
			var view_data = node._data.view;
			var element = view_data.element;
			var topic = node.topic;
			var ncs = getComputedStyle(element);
			this.e_editor.value = topic;

			this.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px';

			//       	element.innerHTML = '';
			element.children[1].innerHTML = '';
			element.appendChild(this.e_editor);
			element.style.zIndex = 5;
			this.e_editor.focus();
			this.e_editor.select();

		},

		edit_node_end: function() {
			if(this.editing_node != null) {
				var node = this.editing_node;
				this.editing_node = null;
				var view_data = node._data.view;
				var element = view_data.element;
				var topic = this.e_editor.value;
				element.style.zIndex = 'auto';
				element.removeChild(this.e_editor);
				if(jm.util.text.is_empty(topic) || node.topic === topic) {
					//未进行编辑输入
					if(this.opts.support_html) {
						// $h(element,node.topic);
						$h(element.children[1], node.topic);

					} else {
						// $t(element,node.topic);
						$h(element.children[1], node.topic);
					}
				} else {
					//已输入编辑内容
					this.jm.update_node(node.id, topic);
				}
			}

		},

		get_view_offset: function() {
			var bounds = this.layout.bounds;
			var _x = (this.size.w - bounds.e - bounds.w) / 2;
			var _y = this.size.h / 2;
			return {
				x: _x,
				y: _y
			};
		},

		resize: function() {
			this.e_canvas.width = 1;
			this.e_canvas.height = 1;
			this.e_nodes.style.width = '1px';
			this.e_nodes.style.height = '1px';

			this.expand_size();
			this._show();
		},

		_show: function() {
			this.e_canvas.width = this.size.w;
			this.e_canvas.height = this.size.h;
			this.e_nodes.style.width = this.size.w + 'px';
			this.e_nodes.style.height = this.size.h + 'px';
			this.show_nodes();
			this.show_lines();
			//this.layout.cache_valid = true;
			this.jm.invoke_event_handle(jm.event_type.resize, {
				data: []
			});
		},

		zoomIn: function() {
			return this.setZoom(this.actualZoom + this.zoomStep);
		},

		zoomOut: function() {
			return this.setZoom(this.actualZoom - this.zoomStep);
		},

		setZoom: function(zoom) {
			if((zoom < this.minZoom) || (zoom > this.maxZoom)) {
				return false;
			}
			this.actualZoom = zoom;
			for(var i = 0; i < this.e_panel.children.length; i++) {
				this.e_panel.children[i].style.transform = 'scale(' + zoom + ')';
			};
			this.show(true);
			return true;

		},

		_center_root: function() {
			// center root node
			var outer_w = this.e_panel.clientWidth;
			var outer_h = this.e_panel.clientHeight;
			if(this.size.w > outer_w) {
				var _offset = this.get_view_offset();
				this.e_panel.scrollLeft = _offset.x - outer_w / 2;
			}
			if(this.size.h > outer_h) {
				this.e_panel.scrollTop = (this.size.h - outer_h) / 2;
			}
		},

		show: function(keep_center) {
			logger.debug('view.show');
			this.expand_size();
			this._show();
			if(!!keep_center) {
				this._center_root();
			}
		},

		relayout: function() {
			this.expand_size();
			this._show();
		},

		save_location: function(node) {
			var vd = node._data.view;
			vd._saved_location = {
				x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
				y: parseInt(vd.element.style.top) - this.e_panel.scrollTop,
			};
		},

		restore_location: function(node) {
			var vd = node._data.view;
			this.e_panel.scrollLeft = parseInt(vd.element.style.left) - vd._saved_location.x;
			this.e_panel.scrollTop = parseInt(vd.element.style.top) - vd._saved_location.y;
		},

		clear_nodes: function() {
			var mind = this.jm.mind;
			if(mind == null) {
				return;
			}
			var nodes = mind.nodes;
			var node = null;
			for(var nodeid in nodes) {
				node = nodes[nodeid];
				node._data.view.element = null;
				node._data.view.expander = null;
			}
			this.e_nodes.innerHTML = '';
		},

		show_nodes: function() {
			var nodes = this.jm.mind.nodes;
			var node = null;
			var node_element = null;
			var expander = null;
			var p = null;
			var p_expander = null;
			var expander_text = '-';
			var view_data = null;
			var _offset = this.get_view_offset();

			for(var nodeid in nodes) {
				node = nodes[nodeid];
				view_data = node._data.view;
				node_element = view_data.element;
				expander = view_data.expander;

				if(!this.layout.is_visible(node)) {
					node_element.style.display = 'none';
					expander.style.display = 'none';
					continue;
				}
				this.reset_node_custom_style(node);
				p = this.layout.get_node_point(node);
				view_data.abs_x = _offset.x + p.x;
				view_data.abs_y = _offset.y + p.y;
				node_element.style.left = (_offset.x + p.x) + 'px';
				node_element.style.top = (_offset.y + p.y) + 'px';
				node_element.style.display = '';
				node_element.style.visibility = 'visible';

				if(!node.isroot && node.children.length > 0) {
					expander_text = node.expanded ? '-' : '+';
					p_expander = this.layout.get_expander_point(node);
					expander.style.left = (_offset.x + p_expander.x) + 'px';
					expander.style.top = (_offset.y + p_expander.y) + 'px';
					expander.style.display = '';
					expander.style.visibility = 'visible';
					$t(expander, expander_text);
				}
				// hide expander while all children have been removed
				if(!node.isroot && node.children.length == 0) {
					expander.style.display = 'none';
					expander.style.visibility = 'hidden';
				}
			}
		},

		reset_node_custom_style: function(node) {
			this._reset_node_custom_style(node._data.view.element, node.data);
		},

		_reset_node_custom_style: function(node_element, node_data) {
			if('background-color' in node_data) {
				node_element.style.backgroundColor = node_data['background-color'];
			}
			if('foreground-color' in node_data) {
				node_element.style.color = node_data['foreground-color'];
			}
			if('width' in node_data) {
				node_element.style.width = node_data['width'] + 'px';
			}
			if('height' in node_data) {
				node_element.style.height = node_data['height'] + 'px';
			}
			if('font-size' in node_data) {
				node_element.style.fontSize = node_data['font-size'] + 'px';
			}
			if('font-weight' in node_data) {
				node_element.style.fontWeight = node_data['font-weight'];
			}
			if('font-style' in node_data) {
				node_element.style.fontStyle = node_data['font-style'];
			}
			if('background-image' in node_data) {
				var backgroundImage = node_data['background-image'];
				if(backgroundImage.startsWith('data') && node_data['width'] && node_data['height']) {
					var img = new Image();

					img.onload = function() {
						var c = $c('canvas');
						c.width = node_element.clientWidth;
						c.height = node_element.clientHeight;
						var img = this;
						if(c.getContext) {
							var ctx = c.getContext('2d');
							ctx.drawImage(img, 2, 2, node_element.clientWidth, node_element.clientHeight);
							var scaledImageData = c.toDataURL();
							node_element.style.backgroundImage = 'url(' + scaledImageData + ')';
						}
					};
					img.src = backgroundImage;

				} else {
					node_element.style.backgroundImage = 'url(' + backgroundImage + ')';
				}
				node_element.style.backgroundSize = '99%';

				if('background-rotation' in node_data) {
					node_element.style.transform = 'rotate(' + node_data['background-rotation'] + 'deg)';
				}

			}
		},

		clear_node_custom_style: function(node) {
			var node_element = node._data.view.element;
			node_element.style.backgroundColor = "";
			node_element.style.color = "";
		},

		clear_lines: function(canvas_ctx) {
			var ctx = canvas_ctx || this.canvas_ctx;
			jm.util.canvas.clear(ctx, 0, 0, this.size.w, this.size.h);
		},

		show_lines: function(canvas_ctx) {
			this.clear_lines(canvas_ctx);
			var nodes = this.jm.mind.nodes;
			var node = null;
			var pin = null;
			var pout = null;
			var _offset = this.get_view_offset();
			for(var nodeid in nodes) {
				node = nodes[nodeid];
				if(!!node.isroot) {
					continue;
				}
				if(('visible' in node._data.layout) && !node._data.layout.visible) {
					continue;
				}
				pin = this.layout.get_node_point_in(node);
				pout = this.layout.get_node_point_out(node.parent);
				this.draw_line(pout, pin, _offset, canvas_ctx);
			}
		},

		draw_line: function(pin, pout, offset, canvas_ctx) {
			var ctx = canvas_ctx || this.canvas_ctx;
			ctx.strokeStyle = this.opts.line_color;
			ctx.lineWidth = this.opts.line_width;
			ctx.lineCap = 'round';

			jm.util.canvas.bezierto(
				ctx,
				pin.x + offset.x,
				pin.y + offset.y,
				pout.x + offset.x,
				pout.y + offset.y);
		},
	};

	// shortcut provider
	jm.shortcut_provider = function(jm, options) {
		this.jm = jm;
		this.opts = options;
		this.mapping = options.mapping;
		this.handles = options.handles;
		this._mapping = {};
	};

	jm.shortcut_provider.prototype = {
		init: function() {
			jm.util.dom.add_event($d, 'keydown', this.handler.bind(this));

			this.handles['addchild'] = this.handle_addchild;
			this.handles['addbrother'] = this.handle_addbrother;
			this.handles['editnode'] = this.handle_editnode;
			this.handles['delnode'] = this.handle_delnode;
			this.handles['toggle'] = this.handle_toggle;
			this.handles['up'] = this.handle_up;
			this.handles['down'] = this.handle_down;
			this.handles['left'] = this.handle_left;
			this.handles['right'] = this.handle_right;

			for(var handle in this.mapping) {
				if(!!this.mapping[handle] && (handle in this.handles)) {
					this._mapping[this.mapping[handle]] = this.handles[handle];
				}
			}
		},

		enable_shortcut: function() {
			this.opts.enable = true;
		},

		disable_shortcut: function() {
			this.opts.enable = false;
		},

		handler: function(e) {
			if(this.jm.view.is_editing()) {
				return;
			}
			var evt = e || event;
			if(!this.opts.enable) {
				return true;
			}
			var kc = evt.keyCode;
			if(kc in this._mapping) {
				this._mapping[kc].call(this, this.jm, e);
			}
		},

		handle_addchild: function(_jm, e) {
			var selected_node = _jm.get_selected_node();
			if(!!selected_node) {
				var nodeid = jm.util.uuid.newid();
				var node = _jm.add_node(selected_node, nodeid, '新节点');
				if(!!node) {
					_jm.select_node(nodeid);
					_jm.begin_edit(nodeid);
				}
			}
		},
		handle_addbrother: function(_jm, e) {
			var selected_node = _jm.get_selected_node();
			if(!!selected_node && !selected_node.isroot) {
				var nodeid = jm.util.uuid.newid();
				var node = _jm.insert_node_after(selected_node, nodeid, '新节点');
				if(!!node) {
					_jm.select_node(nodeid);
					_jm.begin_edit(nodeid);
				}
			}
		},
		handle_editnode: function(_jm, e) {
			var selected_node = _jm.get_selected_node();
			if(!!selected_node) {
				_jm.begin_edit(selected_node);
			}
		},
		handle_delnode: function(_jm, e) {
			var selected_node = _jm.get_selected_node();
			if(!!selected_node && !selected_node.isroot) {
				_jm.select_node(selected_node.parent);
				_jm.remove_node(selected_node);
			}
		},
		handle_toggle: function(_jm, e) {
			var evt = e || event;
			var selected_node = _jm.get_selected_node();
			if(!!selected_node) {
				_jm.toggle_node(selected_node.id);
				evt.stopPropagation();
				evt.preventDefault();
			}
		},
		handle_up: function(_jm, e) {
			var evt = e || event;
			var selected_node = _jm.get_selected_node();
			if(!!selected_node) {
				var up_node = _jm.find_node_before(selected_node);
				if(!up_node) {
					var np = _jm.find_node_before(selected_node.parent);
					if(!!np && np.children.length > 0) {
						up_node = np.children[np.children.length - 1];
					}
				}
				if(!!up_node) {
					_jm.select_node(up_node);
				}
				evt.stopPropagation();
				evt.preventDefault();
			}
		},

		handle_down: function(_jm, e) {
			var evt = e || event;
			var selected_node = _jm.get_selected_node();
			if(!!selected_node) {
				var down_node = _jm.find_node_after(selected_node);
				if(!down_node) {
					var np = _jm.find_node_after(selected_node.parent);
					if(!!np && np.children.length > 0) {
						down_node = np.children[0];
					}
				}
				if(!!down_node) {
					_jm.select_node(down_node);
				}
				evt.stopPropagation();
				evt.preventDefault();
			}
		},

		handle_left: function(_jm, e) {
			this._handle_direction(_jm, e, jm.direction.left);
		},
		handle_right: function(_jm, e) {
			this._handle_direction(_jm, e, jm.direction.right);
		},
		_handle_direction: function(_jm, e, d) {
			var evt = e || event;
			var selected_node = _jm.get_selected_node();
			var node = null;
			if(!!selected_node) {
				if(selected_node.isroot) {
					var c = selected_node.children;
					var children = [];
					for(var i = 0; i < c.length; i++) {
						if(c[i].direction === d) {
							children.push(i)
						}
					}
					node = c[children[Math.floor((children.length - 1) / 2)]];
				} else if(selected_node.direction === d) {
					var children = selected_node.children;
					var childrencount = children.length;
					if(childrencount > 0) {
						node = children[Math.floor((childrencount - 1) / 2)]
					}
				} else {
					node = selected_node.parent;
				}
				if(!!node) {
					_jm.select_node(node);
				}
				evt.stopPropagation();
				evt.preventDefault();
			}
		},
	};

	// plugin
	jm.plugin = function(name, init) {
		this.name = name;
		this.init = init;
	};

	jm.plugins = [];

	jm.register_plugin = function(plugin) {
		if(plugin instanceof jm.plugin) {
			jm.plugins.push(plugin);
		}
	};

	jm.init_plugins = function(sender) {
		$w.setTimeout(function() {
			jm._init_plugins(sender);
		}, 0);
	};

	jm._init_plugins = function(sender) {
		var l = jm.plugins.length;
		var fn_init = null;
		for(var i = 0; i < l; i++) {
			fn_init = jm.plugins[i].init;
			if(typeof fn_init === 'function') {
				fn_init(sender);
			}
		}
	};

	// quick way
	jm.show = function(options, mind) {
		var _jm = new jm(options);
		_jm.show(mind);
		return _jm;
	};

	// export jsmind
	if(typeof module !== 'undefined' && typeof exports === 'object') {
		module.exports = jm;
	} else if(typeof define === 'function' && (define.amd || define.cmd)) {
		define(function() {
			return jm;
		});
	} else {
		$w[__name__] = jm;
	}
})(typeof window !== 'undefined' ? window : global);